﻿// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Globalization;
using Microsoft.Identity.Client.Internal;
using Microsoft.Identity.Client.PlatformsCommon.Factories;
using Microsoft.Identity.Client.Utils;
#if SUPPORTS_SYSTEM_TEXT_JSON
using System.Text.Json;
using System.Text.Encodings.Web;
using System.Text.Unicode;
using JObject = System.Text.Json.Nodes.JsonObject;
#else
using Microsoft.Identity.Json;
using Microsoft.Identity.Json.Linq;
#endif

namespace Microsoft.Identity.Client
{
    /// <summary>
    /// Base exception type thrown when an error occurs during token acquisition.
    /// For more details, see https://aka.ms/msal-net-exceptions
    /// </summary>
    /// <remarks>Avoid throwing this exception. Instead throw the more specialized <see cref="MsalClientException"/>
    /// or <see cref="MsalServiceException"/>
    /// </remarks>
    public class MsalException : Exception
    {
        /// <summary>
        /// An <see cref="AdditionalExceptionData"/> property key, available when using desktop brokers.
        /// </summary>
        public const string BrokerErrorContext = "BrokerErrorContext";
        /// <summary>
        /// An <see cref="AdditionalExceptionData"/> property key, available when using desktop brokers.
        /// </summary>
        public const string BrokerErrorTag = "BrokerErrorTag";
        /// <summary>
        /// An <see cref="AdditionalExceptionData"/> property key, available when using desktop brokers.
        /// </summary>
        public const string BrokerErrorStatus = "BrokerErrorStatus";
        /// <summary>
        /// An <see cref="AdditionalExceptionData"/> property key, available when using desktop brokers.
        /// </summary>
        public const string BrokerErrorCode = "BrokerErrorCode";
        /// <summary>
        /// An <see cref="AdditionalExceptionData"/> property key, available when using desktop brokers.
        /// </summary>
        public const string BrokerTelemetry = "BrokerTelemetry";
        /// <summary>
        /// An <see cref="AdditionalExceptionData"/> property key, available when using managed identity.
        /// </summary>
        public const string ManagedIdentitySource = "ManagedIdentitySource";

        private string _errorCode;

        /// <summary>
        /// Indicates if the previous operation that resulted in this exception should be retried.
        /// </summary>
        public bool IsRetryable { get; set; }

        /// <summary>
        /// Initializes a new instance of the exception class.
        /// </summary>
        public MsalException()
            : base(MsalErrorMessage.Unknown)
        {
            ErrorCode = MsalError.UnknownError;
        }

        /// <summary>
        /// Initializes a new instance of the exception class with a specified
        /// error code.
        /// </summary>
        /// <param name="errorCode">
        /// The error code returned by the service or generated by the client. This is the code you can rely on
        /// for exception handling.
        /// </param>
        public MsalException(string errorCode)
        {
            ErrorCode = errorCode;
        }

        /// <summary>
        /// Initializes a new instance of the exception class with a specified
        /// error code and error message.
        /// </summary>
        /// <param name="errorCode">
        /// The error code returned by the service or generated by the client. This is the code you can rely on
        /// for exception handling.
        /// </param>
        /// <param name="errorMessage">The error message that explains the reason for the exception.</param>
        public MsalException(string errorCode, string errorMessage)
            : base(errorMessage)
        {
            if (string.IsNullOrWhiteSpace(Message))
            {
                throw new ArgumentNullException(nameof(errorMessage));
            }
            ErrorCode = errorCode;
        }

        /// <summary>
        /// Initializes a new instance of the exception class with a specified
        /// error code and a reference to the inner exception that is the cause of
        /// this exception.
        /// </summary>
        /// <param name="errorCode">
        /// The error code returned by the service or generated by the client. This is the code you can rely on
        /// for exception handling.
        /// </param>
        /// <param name="errorMessage">The error message that explains the reason for the exception.</param>
        /// <param name="innerException">
        /// The exception that is the cause of the current exception, or a null reference if no inner
        /// exception is specified.
        /// </param>
        public MsalException(string errorCode, string errorMessage, Exception innerException)
            : base(errorMessage, innerException)
        {
            if (string.IsNullOrWhiteSpace(Message))
            {
                throw new ArgumentNullException(nameof(errorMessage));
            }

            ErrorCode = errorCode;
        }

        /// <summary>
        /// Gets the protocol error code returned by the service or generated by the client. This is the code you can rely on for
        /// exception handling. Values for this code are typically provided in constant strings in the derived exceptions types
        /// with explanations of mitigation.
        /// </summary>
        public string ErrorCode
        {
            get => _errorCode;
            private set
            {
                _errorCode = string.IsNullOrWhiteSpace(value) ?
                    throw new ArgumentNullException("ErrorCode") :
                    value;
            }
        }

        /// <summary>
        /// An ID that can used to piece up a single authentication flow.
        /// </summary>
        public string CorrelationId { get; set; }

        /// <summary>
        /// A property bag with extra details for this exception.
        /// </summary>
        public IReadOnlyDictionary<string, string> AdditionalExceptionData { get; set; }
            = CollectionHelpers.GetEmptyDictionary<string, string>();

        /// <summary>
        /// Creates and returns a string representation of the current exception.
        /// </summary>
        /// <returns>A string representation of the current exception.</returns>
        public override string ToString()
        {
            string msalProductName = PlatformProxyFactory.CreatePlatformProxy(null).GetProductName();
            string msalVersion = MsalIdHelper.GetMsalVersion();

            string innerExceptionContents = InnerException == null
                ? string.Empty
                : $"\nInner Exception: {InnerException}";

            return $"""
                    {msalProductName}.{msalVersion}.{GetType().Name}:
                    	ErrorCode: {ErrorCode}
                    {base.ToString()}{innerExceptionContents}
                    """;
        }

        #region Serialization
        private class ExceptionSerializationKey
        {
            internal const string ExceptionTypeKey = "type";
            internal const string ErrorCodeKey = "error_code";
            internal const string ErrorDescriptionKey = "error_description";
            internal const string AdditionalExceptionData = "additional_exception_data";
            internal const string BrokerErrorContext = "broker_error_context";
            internal const string BrokerErrorTag = "broker_error_tag";
            internal const string BrokerErrorStatus = "broker_error_status";
            internal const string BrokerErrorCode = "broker_error_code";
            internal const string BrokerTelemetry = "broker_telemetry";
            internal const string ManagedIdentitySource = "managed_identity_source";
        }

        internal virtual void PopulateJson(JObject jObject)
        {
            jObject[ExceptionSerializationKey.ExceptionTypeKey] = GetType().Name;
            jObject[ExceptionSerializationKey.ErrorCodeKey] = ErrorCode;
            jObject[ExceptionSerializationKey.ErrorDescriptionKey] = Message;

            // Populate JSON string with broker exception data
            var exceptionData = new JObject();

            if (AdditionalExceptionData.TryGetValue(BrokerErrorContext, out string brokerErrorContext))
            {
                exceptionData[ExceptionSerializationKey.BrokerErrorContext] = brokerErrorContext;
            }
            if (AdditionalExceptionData.TryGetValue(BrokerErrorTag, out string brokerErrorTag))
            {
                exceptionData[ExceptionSerializationKey.BrokerErrorTag] = brokerErrorTag;
            }
            if (AdditionalExceptionData.TryGetValue(BrokerErrorStatus, out string brokerErrorStatus))
            {
                exceptionData[ExceptionSerializationKey.BrokerErrorStatus] = brokerErrorStatus;
            }
            if (AdditionalExceptionData.TryGetValue(BrokerErrorCode, out string brokerErrorCode))
            {
                exceptionData[ExceptionSerializationKey.BrokerErrorCode] = brokerErrorCode;
            }
            if (AdditionalExceptionData.TryGetValue(BrokerTelemetry, out string brokerTelemetry))
            {
                exceptionData[ExceptionSerializationKey.BrokerTelemetry] = brokerTelemetry;
            }
            if(AdditionalExceptionData.TryGetValue(ManagedIdentitySource, out string managedIdentitySource))
            {
                exceptionData[ExceptionSerializationKey.ManagedIdentitySource] = managedIdentitySource;
            }

            jObject[ExceptionSerializationKey.AdditionalExceptionData] = exceptionData;
        }

        internal virtual void PopulateObjectFromJson(JObject jObject)
        {
            // Populate this exception instance with broker exception data from JSON
            var exceptionData = JsonHelper.ExtractInnerJsonAsDictionary(jObject, ExceptionSerializationKey.AdditionalExceptionData);

            if (exceptionData.TryGetValue(ExceptionSerializationKey.BrokerErrorContext, out string brokerErrorContext))
            {
                exceptionData[BrokerErrorContext] = brokerErrorContext;
                exceptionData.Remove(ExceptionSerializationKey.BrokerErrorContext);
            }
            if (exceptionData.TryGetValue(ExceptionSerializationKey.BrokerErrorTag, out string brokerErrorTag))
            {
                exceptionData[BrokerErrorTag] = brokerErrorTag;
                exceptionData.Remove(ExceptionSerializationKey.BrokerErrorTag);
            }
            if (exceptionData.TryGetValue(ExceptionSerializationKey.BrokerErrorStatus, out string brokerErrorStatus))
            {
                exceptionData[BrokerErrorStatus] = brokerErrorStatus;
                exceptionData.Remove(ExceptionSerializationKey.BrokerErrorStatus);
            }
            if (exceptionData.TryGetValue(ExceptionSerializationKey.BrokerErrorCode, out string brokerErrorCode))
            {
                exceptionData[BrokerErrorCode] = brokerErrorCode;
                exceptionData.Remove(ExceptionSerializationKey.BrokerErrorCode);
            }
            if (exceptionData.TryGetValue(ExceptionSerializationKey.BrokerTelemetry, out string brokerTelemetry))
            {
                exceptionData[BrokerTelemetry] = brokerTelemetry;
                exceptionData.Remove(ExceptionSerializationKey.BrokerTelemetry);
            }
            if(exceptionData.TryGetValue(ExceptionSerializationKey.ManagedIdentitySource, out string managedIdentitySource))
            {
                exceptionData[ManagedIdentitySource] = managedIdentitySource;
                exceptionData.Remove(ExceptionSerializationKey.ManagedIdentitySource);
            }

            AdditionalExceptionData = (IReadOnlyDictionary<string, string>)exceptionData;
        }

        /// <summary>
        /// Allows serialization of most values of the exception into JSON.
        /// </summary>
        /// <returns></returns>
        public string ToJsonString()
        {
            JObject jObject = new JObject();
            PopulateJson(jObject);

#if SUPPORTS_SYSTEM_TEXT_JSON
            // By default, STJ is more restrictive (escapes more characters) than Newtonsoft,
            // so the telemetry string is less readable.
            // Relax the encoding rules to match Newtonsoft behavior.
            return jObject.ToJsonString(new JsonSerializerOptions()
            {
                WriteIndented = true,
                Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
            });
#else
            return jObject.ToString();
#endif
        }

        /// <summary>
        /// Allows re-hydration of the MsalException (or one of its derived types) from JSON generated by ToJsonString().
        /// </summary>
        /// <param name="json"></param>
        /// <returns></returns>
        public static MsalException FromJsonString(string json)
        {
            JObject jObject = JsonHelper.ParseIntoJsonObject(json);
            string type = JsonHelper.GetValue<string>(jObject[ExceptionSerializationKey.ExceptionTypeKey]);

            string errorCode = JsonHelper.GetExistingOrEmptyString(jObject, ExceptionSerializationKey.ErrorCodeKey);
            string errorMessage = JsonHelper.GetExistingOrEmptyString(jObject, ExceptionSerializationKey.ErrorDescriptionKey);

            MsalException ex = type switch
            {
                nameof(MsalException) => new MsalException(errorCode, errorMessage),
                nameof(MsalClientException) => new MsalClientException(errorCode, errorMessage),
                nameof(MsalServiceException) => new MsalServiceException(errorCode, errorMessage),
                nameof(MsalUiRequiredException) => new MsalUiRequiredException(errorCode, errorMessage),
                _ => throw new MsalClientException(MsalError.JsonParseError, MsalErrorMessage.MsalExceptionFailedToParse),
            };

            ex.PopulateObjectFromJson(jObject);
            return ex;
        }

        #endregion
    }
}
